package cn.coder.toolkit;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

/**
 * 常见传参方式
 * Get: 查询字符串
 * Post: 查询字符串 / 表单提交(application/x-www-form-urlencoded) / 表单上传(multipart/form-data) / JSON(application/json) / TEXT(text/plain) / XML(application/xml) / 二进制流(application/octet-stream)
 */
@Slf4j
public final class HttpKit {

    private HttpKit() {}

    private static final HttpClient CLIENT = getClient(10);

    /**
     * 生成默认的 HttpClient 对象, 可通过 HttpClient.Builder 自行创建
     *
     * @param seconds 连接超时时间, 单位秒, client 只负责连接超时, 具体读写超时由对应 HttpRequest 负责
     */
    public static HttpClient getClient(int seconds) {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new DefaultX509TrustManager()}, new SecureRandom());
            return HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(seconds)) // 连接超时
                    .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL)) // Cookie 自动处理
                    .sslContext(sslContext) // 绕过 Https 相关的证书域名验证
                    .followRedirects(HttpClient.Redirect.ALWAYS) // 重定向
                    .build();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException(e);
        }
    }

    public static List<HttpCookie> getCookies(HttpClient client) {
        return ((CookieManager) client.cookieHandler()
                .orElseThrow(() -> new RuntimeException("CookieHandler is null")))
                .getCookieStore()
                .getCookies();
    }

    public static List<HttpCookie> getCookies(HttpClient client, URI uri) {
        return ((CookieManager) client.cookieHandler()
                .orElseThrow(() -> new RuntimeException("CookieHandler is null")))
                .getCookieStore()
                .get(uri);
    }

    public static GetBuilder<Get> get() {
        return new Get(CLIENT);
    }

    public static GetBuilder<Get> get(HttpClient client) {
        return new Get(client);
    }

    public static PostBuilder<Post> post() {
        return new Post(CLIENT);
    }

    public static PostBuilder<Post> post(HttpClient client) {
        return new Post(client);
    }

    // ----------------------------------------------
    // interface: HttpBuilder & RequestBuilder & GetBuilder & PostBuilder
    // ----------------------------------------------

    public interface HttpBuilder<T extends HttpBuilder<T>> {
        <E> HttpResponse<E> sync(HttpResponse.BodyHandler<E> handler);
        HttpResponse<String> syncString();
        <E> CompletableFuture<HttpResponse<E>> async(HttpResponse.BodyHandler<E> handler);
        CompletableFuture<HttpResponse<String>> asyncString();
    }

    public interface RequestBuilder<T extends RequestBuilder<T>> extends HttpBuilder<T> {
        T timeout(int seconds);
        T url(String url);
        T header(String key, String value, String... values);
        T headers(Map<String, String> headers);
        T headerS(Map<String, List<String>> headers);
        T cookie(String value);
        T contentType(String value);
    }

    public interface GetBuilder<T extends GetBuilder<T>> extends RequestBuilder<T> {
        GetBuilder<T> parameter(String key, Object value, Object... values);
        GetBuilder<T> parameters(Map<String, Object> parameters);
        GetBuilder<T> parameterS(Map<String, List<Object>> parameters);
    }

    public interface PostBuilder<T extends PostBuilder<T>> extends RequestBuilder<T> {
        PostBuilder<T> body(HttpRequest.BodyPublisher body);
        PostBuilder<T> body(HttpRequest.BodyPublisher body, String contentType);
        PostBuilder<T> string(String string, Charset charset, String contentType);
        PostBuilder<T> string(String string, String contentType);
        PostBuilder<T> json(String json, Charset charset);
        PostBuilder<T> json(String json);
        PostBuilder<T> text(String text, Charset charset);
        PostBuilder<T> text(String text);
        PostBuilder<T> xml(String xml, Charset charset);
        PostBuilder<T> xml(String xml);
        PostBuilder<T> bytes(byte[] bytes);
        PostBuilder<T> path(Path path);
        PostBuilder<T> inputStream(InputStream inputStream);
        PostBuilder<T> parameters(Map<String, List<Object>> parameters, Charset charset);
        PostBuilder<T> parameters(Map<String, List<Object>> parameters);
        PostBuilder<T> paths(Map<String, List<Path>> paths);
    }

    // ----------------------------------------------
    // implements: AbstractRequest & Get & Post
    // ----------------------------------------------

    public abstract static class AbstractRequest<T extends RequestBuilder<T>> implements RequestBuilder<T> {

        protected static final String CONTENT_TYPE = "Content-Type";

        protected final HttpClient client;

        protected String url;
        protected Duration timeout = Duration.ofSeconds(60);
        protected final Map<String, List<String>> headers = new TreeMap<>();

        protected AbstractRequest(HttpClient client) {
            this.client = client;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T timeout(int seconds) {
            this.timeout = Duration.ofSeconds(seconds);
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T url(String url) {
            this.url = url;
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T header(String key, String value, String... values) {
            List<String> list = this.headers.computeIfAbsent(key, k -> new ArrayList<>());
            list.add(value);
            if (values != null && values.length > 0) {
                Collections.addAll(list, values);
            }
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T headers(Map<String, String> headers) {
            headers.forEach((key, value) -> this.headers.computeIfAbsent(key, k -> new LinkedList<>()).add(value));
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T headerS(Map<String, List<String>> headers) {
            headers.forEach((key, list) -> this.headers.computeIfAbsent(key, k -> new LinkedList<>()).addAll(list));
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T cookie(String value) {
            this.headers.computeIfAbsent("Cookie", k -> new LinkedList<>()).add(value);
            return (T) this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T contentType(String value) {
            this.headers.computeIfAbsent(CONTENT_TYPE, k -> new LinkedList<>()).clear();
            this.headers.get(CONTENT_TYPE).add(value);
            return (T) this;
        }

        protected String urlEncode(String value, Charset charset) {
            return URLEncoder.encode(value, charset);
        }

        protected String urlEncode(String value) {
            return URLEncoder.encode(value, StandardCharsets.UTF_8);
        }

    }

    public static final class Get extends AbstractRequest<Get> implements GetBuilder<Get> {

        private final Map<String, List<Object>> parameters = new TreeMap<>();

        private Get(HttpClient client) {
            super(client);
        }

        @Override
        public GetBuilder<Get> parameter(String key, Object value, Object... values) {
            List<Object> list = this.parameters.computeIfAbsent(key, k -> new ArrayList<>());
            list.add(value);
            if (values != null && values.length > 0) {
                Collections.addAll(list, values);
            }
            return this;
        }

        @Override
        public GetBuilder<Get> parameters(Map<String, Object> parameters) {
            parameters.forEach((key, value) -> this.parameters.computeIfAbsent(key, k -> new LinkedList<>()).add(value));
            return this;
        }

        @Override
        public GetBuilder<Get> parameterS(Map<String, List<Object>> parameters) {
            parameters.forEach((key, list) -> this.parameters.computeIfAbsent(key, k -> new LinkedList<>()).addAll(list));
            return this;
        }

        private HttpRequest buildHttpRequest() {
            List<String> pairList = new LinkedList<>();
            parameters.forEach((key, list) -> {
                String encodedKey = urlEncode(key);
                list.forEach(value -> pairList.add(encodedKey + "=" + urlEncode(String.valueOf(value))));
            });
            String query = String.join("&", pairList);
            String delimiter = this.url.contains("?") ? "&" : "?";
            String finalUrl = query.isEmpty() ? url : url + delimiter + query;
            HttpRequest.Builder builder = HttpRequest.newBuilder().timeout(this.timeout).uri(URI.create(finalUrl));
            this.headers.forEach((key, list) -> list.forEach(value -> builder.header(key, value)));
            return builder.GET().build();
        }

        @Override
        public <E> HttpResponse<E> sync(HttpResponse.BodyHandler<E> handler) {
            try {
                return this.client.send(buildHttpRequest(), handler);
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public HttpResponse<String> syncString() {
            return sync(HttpResponse.BodyHandlers.ofString());
        }

        @Override
        public <E> CompletableFuture<HttpResponse<E>> async(HttpResponse.BodyHandler<E> handler) {
            return this.client.sendAsync(buildHttpRequest(), handler);
        }

        @Override
        public CompletableFuture<HttpResponse<String>> asyncString() {
            return async(HttpResponse.BodyHandlers.ofString());
        }

    }

    public static final class Post extends AbstractRequest<Post> implements PostBuilder<Post> {

        private HttpRequest.BodyPublisher body;

        private Post(HttpClient client) {
            super(client);
        }

        @Override
        public PostBuilder<Post> body(HttpRequest.BodyPublisher body) {
            this.body = body;
            return this;
        }

        @Override
        public PostBuilder<Post> body(HttpRequest.BodyPublisher body, String contentType) {
            trySetContentType(contentType);
            return body(body);
        }

        @Override
        public PostBuilder<Post> string(String string, Charset charset, String contentType) {
            return body(HttpRequest.BodyPublishers.ofString(string, charset), contentType);
        }

        @Override
        public PostBuilder<Post> string(String string, String contentType) {
            return string(string, StandardCharsets.UTF_8, contentType);
        }

        @Override
        public PostBuilder<Post> json(String json, Charset charset) {
            return string(json, charset, "application/json;charset=" + charset.name().toLowerCase());
        }

        @Override
        public PostBuilder<Post> json(String json) {
            return json(json, StandardCharsets.UTF_8);
        }

        @Override
        public PostBuilder<Post> text(String text, Charset charset) {
            return string(text, charset, "text/plain;charset=" + charset.name().toLowerCase());
        }

        @Override
        public PostBuilder<Post> text(String text) {
            return text(text, StandardCharsets.UTF_8);
        }

        @Override
        public PostBuilder<Post> xml(String xml, Charset charset) {
            return string(xml, charset, "application/xml;charset=" + charset.name().toLowerCase());
        }

        @Override
        public PostBuilder<Post> xml(String xml) {
            return xml(xml, StandardCharsets.UTF_8);
        }

        @Override
        public PostBuilder<Post> bytes(byte[] bytes) {
            return body(HttpRequest.BodyPublishers.ofByteArray(bytes), "application/octet-stream");
        }

        @Override
        public PostBuilder<Post> path(Path path) {
            try {
                return body(HttpRequest.BodyPublishers.ofFile(path), probeContentType(path));
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public PostBuilder<Post> inputStream(InputStream inputStream) {
            return body(HttpRequest.BodyPublishers.ofInputStream(() -> inputStream), "application/octet-stream");
        }

        @Override
        public PostBuilder<Post> parameters(Map<String, List<Object>> parameters, Charset charset) {
            List<String> pairList = new LinkedList<>();
            parameters.forEach((key, list) -> {
                String encodedKey = urlEncode(key, charset);
                list.forEach(value -> pairList.add(encodedKey + "=" + urlEncode(String.valueOf(value), charset)));
            });
            String collect = String.join("&", pairList);
            return string(collect, charset, "application/x-www-form-urlencoded; charset=" + charset.name().toLowerCase());
        }

        @Override
        public PostBuilder<Post> parameters(Map<String, List<Object>> parameters) {
            return parameters(parameters, StandardCharsets.UTF_8);
        }

        @Override
        public PostBuilder<Post> paths(Map<String, List<Path>> paths) {
            MultipartFormDataBodyBuilder builder = MultipartFormDataBodyBuilder.builder();
            paths.forEach((key, list) -> {
                list.forEach(path -> builder.add(key, path));
            });
            this.body = builder.build();
            // 必须强制覆盖
            this.headers.computeIfAbsent(CONTENT_TYPE, k -> new LinkedList<>()).clear();
            this.headers.get(CONTENT_TYPE).add("multipart/form-data; boundary=" + builder.getBoundary());
            return this;
        }

        @Override
        public <E> HttpResponse<E> sync(HttpResponse.BodyHandler<E> handler) {
            try {
                return this.client.send(buildHttpRequest(), handler);
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public HttpResponse<String> syncString() {
            return sync(HttpResponse.BodyHandlers.ofString());
        }

        @Override
        public <E> CompletableFuture<HttpResponse<E>> async(HttpResponse.BodyHandler<E> handler) {
            return this.client.sendAsync(buildHttpRequest(), handler);
        }

        @Override
        public CompletableFuture<HttpResponse<String>> asyncString() {
            return async(HttpResponse.BodyHandlers.ofString());
        }

        /**
         * 构建 HttpRequest
         */
        private HttpRequest buildHttpRequest() {
            HttpRequest.Builder builder = HttpRequest.newBuilder().timeout(this.timeout).uri(URI.create(this.url));
            this.headers.forEach((key, list) -> {
                list.forEach(value -> builder.header(key, value));
            });
            return builder.POST(null != this.body ? this.body : HttpRequest.BodyPublishers.noBody()).build();
        }

        /**
         * 不存在 Content-Type 时候设置
         */
        private void trySetContentType(String contentType) {
            List<String> list = this.headers.get(CONTENT_TYPE);
            if (null == list || list.isEmpty()) {
                this.headers.computeIfAbsent(CONTENT_TYPE, k -> new LinkedList<>()).add(contentType);
            }
        }

        /**
         * 自动探测文件的 Content-Type
         */
        private String probeContentType(Path path) {
            try {
                String type = Files.probeContentType(path);
                return type != null ? type : "application/octet-stream";
            } catch (IOException e) {
                return "application/octet-stream";
            }
        }

    }

    // ----------------------------------------------
    // SSL TrustManager
    // ----------------------------------------------

    private static class DefaultX509TrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }

    // ----------------------------------------------
    // MultipartFormDataBodyBuilder
    // ----------------------------------------------
    
    /**
     * 标准 multipart/form-data 格式, 使用该请求体时, 请求头中需包含 Content-Type: multipart/form-data; boundary=???
     * <p>
     * --boundary
     * Content-Disposition: form-data; name="field1"
     * <p>
     * value1
     * --boundary
     * Content-Disposition: form-data; name="file"; filename="example.txt"
     * Content-Type: text/plain
     * <p>
     * (file 内容)
     * --boundary--
     */
    public static class MultipartFormDataBodyBuilder {

        @Getter
        private final String boundary = UUID.randomUUID().toString().replace("-", "");
        private final List<HttpRequest.BodyPublisher> parts = new LinkedList<>();

        /**
         * 静态 builder
         */
        public static MultipartFormDataBodyBuilder builder() {
            return new MultipartFormDataBodyBuilder();
        }

        /**
         * 添加文本字段
         */
        public MultipartFormDataBodyBuilder add(String name, Object value) {
            String part = "--" + boundary + "\r\n" +
                    "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" +
                    value.toString() + "\r\n";
            parts.add(HttpRequest.BodyPublishers.ofString(part, StandardCharsets.UTF_8));
            return this;
        }

        /**
         * 添加文件字段
         */
        public MultipartFormDataBodyBuilder add(String name, Path path, String contentType) {
            try {
                String fileName = path.getFileName().toString();
                String encodedFileName = percentEncode(fileName);
                String fallbackFileName = isAsciiStr(fileName) ? fileName : "file";
                // 大部分现代服务器和框架会优先解析 filename*, 如果服务器不支持 filename*，则退回使用 filename。
                String prefix = "--" + boundary + "\r\n" +
                        "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + fallbackFileName + "\"; filename*=UTF-8''" + encodedFileName + "\r\n" +
                        "Content-Type: " + contentType + "\r\n\r\n";
                parts.add(HttpRequest.BodyPublishers.ofString(prefix, StandardCharsets.UTF_8));
                parts.add(HttpRequest.BodyPublishers.ofFile(path));
                parts.add(HttpRequest.BodyPublishers.ofString("\r\n"));
                return this;
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 添加文件字段
         */
        public MultipartFormDataBodyBuilder add(String name, Path path) {
            return add(name, path, probeContentType(path));
        }

        /**
         * 添加任意 BodyPublisher
         */
        public MultipartFormDataBodyBuilder add(HttpRequest.BodyPublisher publisher) {
            parts.add(publisher);
            return this;
        }

        /**
         * 最终 build
         */
        public HttpRequest.BodyPublisher build() {
            parts.add(HttpRequest.BodyPublishers.ofString("--" + boundary + "--\r\n"));
            return HttpRequest.BodyPublishers.concat(parts.toArray(new HttpRequest.BodyPublisher[0]));
        }

        /**
         * 百分比编码
         */
        private static String percentEncode(String s) {
            StringBuilder sb = new StringBuilder();
            for (char c : s.toCharArray()) {
                if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
                        || c == '-' || c == '.' || c == '_' || c == '~') {
                    sb.append(c);
                } else {
                    byte[] bytes = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
                    for (byte b : bytes) {
                        sb.append(String.format("%%%02X", b));
                    }
                }
            }
            return sb.toString();
        }

        /**
         * 判断是否 ASCII 字符串
         */
        private boolean isAsciiStr(String string) {
            return string.chars().allMatch(c -> c <= 127);
        }

        /**
         * 自动探测 Content-Type
         */
        private String probeContentType(Path path) {
            try {
                String type = Files.probeContentType(path);
                return type != null ? type : "application/octet-stream";
            } catch (IOException e) {
                return "application/octet-stream";
            }
        }

    }

}
